﻿using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq.Expressions;
using System.Reflection;
using Affinity.Core.Exceptions;
using Affinity.Core.Utility;

namespace Affinity.Synthesis.ExpressionCells.Basis
{
    /// <summary>
    /// Represents a MemberExpression for accessing a property value.
    /// </summary>
    internal class PropertyAccessCell : ExpressionCellBase
    {
        /// <summary>
        /// Constructor accepting the PropertyInfo, compiled delegate corresponding to the property's get accessor,
        /// and MemberExpression for the property access.
        /// </summary>
        /// <param name="property"></param>
        /// <param name="propertyAccessDelegate"></param>
        /// <param name="sourceExpression"></param>
        public PropertyAccessCell(PropertyInfo property, Delegate propertyAccessDelegate, MemberExpression sourceExpression)
            : base(sourceExpression)
        {
            if (property == null) throw new ArgumentNullException("property");
            if (property.GetGetMethod() == null) throw new ArgumentException("Property does not have a visible get method.");
            if (propertyAccessDelegate == null) throw new ArgumentNullException("propertyAccessDelegate");

            //TODO: Refactor to use statically invoked delegate?

            _Property = property;
            _PropertyAccessDelegate = propertyAccessDelegate;
            _IsStatic = property.GetGetMethod().IsStatic;
            //ParameterExpression parameterExpression = Expression.Parameter(property.DeclaringType);
        }

        /// <summary>
        /// Copy constructor.
        /// </summary>
        /// <param name="existing"></param>
        private PropertyAccessCell(PropertyAccessCell existing)
            : base(existing)
        {
            _Property = existing._Property;
            _PropertyAccessDelegate = existing._PropertyAccessDelegate;
            _IsStatic = existing._IsStatic;
        }

        /// <summary>
        /// Copies this cell without state. Note: this functionality should be moved to stateless atoms.
        /// </summary>
        public override IExpressionCell Copy()
        {
            return new PropertyAccessCell(this);
        }

        /// <summary>
        /// Returns a description of this expression cell.
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            return string.Format("{0}: {1}.{2}", base.ToString(), _Property.DeclaringType.Name, _Property.Name);
        }

        /// <summary>
        /// "Calculates" the value of this expression cell by invoking the property access delegate to read
        /// the value of the property.
        /// </summary>
        /// <param name="inputs"></param>
        /// <param name="hasValue"></param>
        /// <param name="output"></param>
        protected override void CalculateOutput(object[] inputs, out bool hasValue, out object output)
        {
            if (_IsStatic)
            {
                if (inputs.Length != 0) throw new ArgumentException(typeof(PropertyAccessCell) + " requires exactly zero input values.");

                hasValue = true;

                try
                {
                    output = _PropertyAccessDelegate.DynamicInvoke();
                }
                catch (Exception ex)
                {
                    ex = ExceptionUtil.GetInnermostException(ex);

                    throw new ExceptionWrapper(new ExceptionWrapper.Parameters
                        {
                            Message = string.Format("Exception encountered while accessing property {0}: {1}", Description, ex.Message),
                            StackTrace = ex.StackTrace
                        });
                }
            }
            else
            {
                if (inputs.Length != 1) throw new ArgumentException(typeof(PropertyAccessCell) + " requires exactly one input value.");

                SetNotifyPropertyChangedSource(inputs[0] as INotifyPropertyChanged);

                if (inputs[0] == null)
                {
                    hasValue = false;
                    output = null;
                }
                else
                {
                    hasValue = true;

                    try
                    {
                        output = _PropertyAccessDelegate.DynamicInvoke(inputs[0]);
                    }
                    catch (Exception ex)
                    {
                        ex = ExceptionUtil.GetInnermostException(ex);

                        throw new ExceptionWrapper(new ExceptionWrapper.Parameters
                            {
                                Message = string.Format("Exception encountered while accessing property {0}: {1}", Description, ex.Message),
                                StackTrace = ex.StackTrace
                            });
                    }
                }
            }
        }
        
        /// <summary>
        /// If the object from which we are reading the property value is of type INotifyPropertyChanged, this method hooks
        /// a listener to the object's PropertyChanged event. That source object originates from an input to this expression cell,
        /// so it can change arbitrarily (it can be different each time CalculateOutput is called).
        /// </summary>
        /// <param name="source"></param>
        private void SetNotifyPropertyChangedSource(INotifyPropertyChanged source)
        {
            if (source != _Source)
            {
                if (_WeakPropertyChangedCallback == null) _WeakPropertyChangedCallback = new WeakPropertyChangedCallback(this);

                if (_Source != null) _WeakPropertyChangedCallback.Remove(_Source);
                _Source = source;
                if (_Source != null) _WeakPropertyChangedCallback.Add(_Source);
            }
        }

        /// <summary>
        /// Handles the PropertyChanged event for the object from which we last read the property value.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void HandlePropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (e.PropertyName == _Property.Name)
            {
                //Debug.WriteLine("Property {0} changed on {1}.", e.PropertyName, sender);
                OnValueDirty();
            }
        }

        private bool _IsStatic = false;
        private PropertyInfo _Property = null;
        private Delegate _PropertyAccessDelegate = null;
        private INotifyPropertyChanged _Source = null;
        private WeakPropertyChangedCallback _WeakPropertyChangedCallback = null;
        
        /// <summary>
        /// Provides a weak event handling mechanism for PropertyAccessCell.
        /// </summary>
        private class WeakPropertyChangedCallback
        {
            /// <summary>
            /// Constructor accepting the owning PropertyAccessCell.
            /// </summary>
            /// <param name="cell"></param>
            public WeakPropertyChangedCallback(PropertyAccessCell cell)
            {
                _WeakCellReference = new WeakReference(cell);
            }

            /// <summary>
            /// Adds a listener to the PropertyChanged event.
            /// </summary>
            /// <param name="notifyPropertyChanged"></param>
            public void Add(INotifyPropertyChanged notifyPropertyChanged)
            {
                notifyPropertyChanged.PropertyChanged += HandlePropertyChanged;
            }

            /// <summary>
            /// Removes a listener from the PropertyChanged event.
            /// </summary>
            /// <param name="notifyPropertyChanged"></param>
            public void Remove(INotifyPropertyChanged notifyPropertyChanged)
            {
                notifyPropertyChanged.PropertyChanged -= HandlePropertyChanged;
            }

            /// <summary>
            /// The handler for INotifyPropertyChanged.PropertyChanged.
            /// If the owning PropertyAccessCell has been garbage collected, removes its listener from PropertyChanged.
            /// Otherwise, calls HandlePropertyChanged on its owning PropertyAccessCell.
            /// </summary>
            /// <param name="sender"></param>
            /// <param name="e"></param>
            private void HandlePropertyChanged(object sender, PropertyChangedEventArgs e)
            {
                PropertyAccessCell strongCellReference = (PropertyAccessCell)_WeakCellReference.Target;
                if (strongCellReference == null) Remove((INotifyPropertyChanged)sender);
                else strongCellReference.HandlePropertyChanged(sender, e);
            }

            private readonly WeakReference _WeakCellReference = null;
        }
    }
}
